# Цикл событий (event loop)
# event loop — сердце любого приложения на asyncio.
# Мы уже много раз упоминали про цикл событий, в котором находятся созданные задачи. В данном уроке рассмотрим более подробно, как он работает. 
# Цикл событий, или event loop, это сердце любого асинхронного кода. Его можно представить как обычный цикл, который работает в одном потоке
# и занимается организацией задач: он отмечает, когда какая задача должна начаться, и запускает её в нужный момент. Это делает работу вашего
# кода более простой и предсказуемой, поскольку всё происходит последовательно, без хаоса от множества одновременно работающих частей.

# Цикл событий следит за всем, что нужно сделать, и когда какая-то задача готова к выполнению, он берёт её и выполняет. Благодаря циклу
# событий можно эффективно распределять задачи без необходимости ждать, пока одна закончится, прежде чем начать другую.

# В его основе лежит очень простая идея. Мы создаём список или очередь событий и задач, а затем запускаем цикл, который не останавливается и
# обрабатывает эти задачи одну за другой по мере их появления. Такой механизм помогает держать всё под контролем и следит, чтобы каждая задача
# была выполнена в правильный момент.


# Базовый цикл событий мог бы выглядеть следующим образом:
# Данный код — всего лишь условность, реальный цикл событий будет выглядеть куда длиннее и запутаннее.
#Пример ниже показывает, как мог бы выглядеть простой цикл событий. 
# class SimpleEventLoop:
#     def __init__(self):
#         self.tasks = []               # Очередь задач

#     def add_task(self, task):
#         self.tasks.append(task)       # Добавление задачи в очередь

#     def run_forever(self):
#         while self.tasks:             # Выполнять, пока есть задачи
#             task = self.tasks.pop(0)  # Получить первую задачу
#             task()                    # Выполнить задачу


# def task1():
#     print("Выполняется задача 1")

# def task2():
#     print("Выполняется задача 2")

# loop = SimpleEventLoop()
# loop.add_task(task1)  # Добавить задачу 1 в цикл событий
# loop.add_task(task2)  # Добавить задачу 2 в цикл событий
# loop.run_forever()    # Запустить цикл событий

# В этом примере:

# Мы создали простейший класс SimpleEventLoop, который имитирует логику работы цикла событий.
# В классе есть метод add_task(), который добавляет новую задачу в очередь.
# Метод run_forever() запускает цикл, который выполняется, пока есть задачи. Задачи извлекаются из очереди и выполняются одна за другой.
# Задачи представлены в виде функций task1() и task2(), которые просто печатают сообщения о своём выполнении.

# В данном примере не учтена возможность отложить задачу и переключиться на следующую. В реальном цикле событий  реализован данный функционал:
# задача может вернуть управление в цикл событий, который сразу передаст управление другой задаче.  


# Аналогия из реальной жизни:
# Event loop можно сравнить с координатором в пиццерии.

# Представьте, что у вас есть пиццерия, где клиенты делают заказы, а пиццы готовятся в определенном порядке. Здесь координатор (event loop)
# отвечает за обработку всех заказов и отправку их на выполнение.

# Клиенты (код, который вызывает асинхронные функции) делают заказы (создают задачи) и передают их координатору;
# Координатор (event loop) заносит заказы (задачи) в очередь и обрабатывает их по порядку;
# Если какой-то заказ (задача) требует времени для выполнения (например, ожидание ответа от внешнего источника данных - API, БД и т.д.),
# координатор ставит его на паузу и переходит к следующему заказу (задаче), чтобы оптимально использовать время и ресурсы;
# Когда заказы, требующие ожидания, завершаются, координатор продолжает их обработку и возвращает результаты клиентам.

# Работа координатора пиццерии (event loop):

# Координатор получает заказ на пиццу;
# Координатор отправляет заказ повару и переходит к следующему заказу;
# Пока повар готовит пиццу, координатор может принять и обработать другие заказы.

# Теперь переведем это на язык программирования:

# Event loop начинает выполнять асинхронную задачу (например, запрос к API);
# Вместо блокирования всего потока и ожидания завершения текущей задачи, event loop ставит ее на паузу
# (при помощи ключевого слова await);
# Event loop переключается на другие доступные задачи, продолжая их выполнение;
# Когда задача, которая была поставлена на паузу, завершается (например, получен ответ от API), event loop возвращается к
# ней и продолжает выполнение с того места, где остановился.

# Таким образом, event loop позволяет конкурентно обрабатывать множество асинхронных задач, ставя на паузу те из них, которые
# требуют ожидания, и переключаясь между задачами для эффективного использования ресурсов и времени. Это повышает производительность
# программы и предотвращает блокировку в случае длительных операций.

#Давайте напишем небольшую программу, которая будет использовать концепцию event loop для имитации работы пиццерии.
import asyncio
import random

class Pizzeria:
    def __init__(self, name):
        self.name = name

    async def make_pizza(self, order_id):
        cook_time = random.randint(2, 5)      # случайное время готовки пиццы от 2 до 5 секунд
        print(f'Пиццерия {self.name} начала готовить пиццу для заказа {order_id}.')
        await asyncio.sleep(cook_time)        # ожидание пока пицца готовится
        print(f'Пиццерия {self.name} закончила готовить пиццу для заказа {order_id}.')

async def main():
    pizzeria = Pizzeria("Тесто & Сыр")

    # создание 5 заказов
    tasks = [pizzeria.make_pizza(i) for i in range(1, 6)]

    # запуск всех задач (заказов) в Event Loop
    await asyncio.gather(*tasks)

asyncio.run(main())
# В этом примере у нас есть класс Pizzeria с асинхронным методом make_pizza(), который имитирует процесс приготовления
# пиццы. Метод make_pizza() использует asyncio.sleep() для имитации затрат времени на приготовление пиццы. Этот вызов
# sleep() является точкой, где Event Loop может приостановить выполнение текущей задачи и перейти к следующей задаче в очереди.

# В функции main() мы создаем несколько задач приготовления пиццы и используем asyncio.gather() для запуска всех этих задач
# в Event Loop. Event Loop затем отслеживает все эти задачи и переключается между ними, когда они ожидают, что пицца будет готова,
# увеличивая таким образом эффективность обработки заказов.

# Условно event loop обрабатывает события следующим образом:

# Сбор событий: Цикл событий отслеживает и собирает асинхронные события и задачи, которые нужно выполнить. Это включает в себя операции ввода/вывода,
# таймеры и запланированные асинхронные задачи. События и задачи могут поступать из различных источников и добавляются в очередь на обработку.

# Определение и распределение задач: Когда событие готово к обработке, event loop определяет соответствующую асинхронную задачу или корутину 
# для его обработки. Приоритеты событий обычно определяются автоматически системой.

# Выполнение задач: Event loop запускает задачу и управляет её выполнением. Если задача асинхронна, она может быть приостановлена (await) для 
# ожидания других событий (например, ответа от сервера) без блокировки цикла событий. Таким образом, event loop может переключаться между задачами,
# эффективно распределяя время процессора.

# Ожидание и завершение событий: После инициации задачи event loop продолжает отслеживать и обрабатывать другие входящие события. Он циклически 
# возвращается к шагу сбора событий, пока есть активные или ожидающие задачи.

# Обработка ошибок: Если во время выполнения задачи возникает ошибка, она должна быть обработана внутри самой задачи или через механизмы обработки 
# исключений asyncio. Цикл событий продолжит работу, если исключение будет корректно обработано. Необработанные исключения могут прервать выполнение
# задачи, но не обязательно останавливают сам цикл событий.

# Перезапуск цикла: Цикл событий продолжает работать до тех пор, пока не будет остановлен явно, либо до тех пор, пока не завершатся все задачи. Это 
# позволяет приложению оставаться реактивным и обрабатывать события в реальном времени.


# Также стоит понимать, что это условная структура цикла событий. Точная реализация может быть гораздо сложнее и включать в себя различные стадии,
# такие как обработка микрозадач или обработка событий различных типов в определенном порядке.

# Также стоит учитывать, что все события, передаваемые в цикл событий, должны быть awaitable объектами, умеющими переключать контекст выполнения
# в нужном месте. Нельзя поместить в цикл событий обычную синхронную функцию. Но, несмотря на это,
# использовать синхронную функцию внутри корутины допустимо.


# Пример такого кода:

import asyncio

async def async_func():
    def sync_func():

        # тут может быть любой синхронный код

        return 42

    result = sync_func()
    print(result)

asyncio.run(async_func())

